// ==UserScript==
// @name         Geocaching: Show UserSuppliedContent
// @namespace    http://tampermonkey.net/
// @version      3.8
// @description  Shows UserSuppliedContent source, highlights hidden clues, links GC codes, coordinates, href/src, and HTML comments
// @copyright    2026
// @author       OUT14ND3R
// @match        https://www.geocaching.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const SELECTOR_USC = 'div.UserSuppliedContent';
    const IDS = {
        button: 'btnShowSource',
        popup: 'usc_popup',
        content: 'usc_content',
        close: 'usc_float_close',
        style: 'usc_source_style'
    };

    const state = {
        popup: null,
        mode: 'source',
        combinedHTML: '',
        hasUSC: false
    };

    function addStyles() {
        if (document.getElementById(IDS.style)) return;

        const style = document.createElement('style');
        style.id = IDS.style;
        style.textContent = `
            #${IDS.button} {
                color: red;
                float: right;
                margin-left: 10px;
                cursor: pointer;
            }
            #${IDS.popup} {
                position: fixed;
                top: 120px;
                right: 20px;
                width: 560px;
                height: 460px;
                min-width: 260px;
                min-height: 160px;
                z-index: 999999999;
                background: white;
                border: 3px solid #333;
                border-radius: 6px;
                box-shadow: 0 0 12px rgba(0, 0, 0, 0.6);
                padding: 10px;
                overflow: auto;
                resize: both !important;
            }
            #${IDS.popup}[hidden], #${IDS.close}[hidden] {
                display: none !important;
            }
            #${IDS.popup} .usc-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                gap: 6px;
                margin-bottom: 8px;
            }
            #${IDS.popup} .usc-title {
                font-weight: bold;
                margin-right: auto;
            }
            #${IDS.popup} .usc-btn {
                cursor: pointer;
            }
            #${IDS.popup} .usc-close-btn {
                color: white;
                background: red;
                border: 1px solid #900;
                border-radius: 4px;
                padding: 2px 8px;
                cursor: pointer;
            }
            #${IDS.popup} .usc-pre {
                white-space: pre-wrap;
                word-break: break-word;
                font-family: monospace;
                font-size: 13px;
            }
            #${IDS.popup} .usc-comments {
                white-space: pre-wrap;
                background: #eaffea;
                padding: 8px;
                border: 1px solid #8c8;
            }
            #${IDS.close} {
                position: fixed;
                top: 90px;
                right: 20px;
                width: 24px;
                height: 24px;
                z-index: 1000000000;
                border-radius: 4px;
                background: #333;
                color: white;
                font-size: 18px;
                font-weight: bold;
                line-height: 24px;
                text-align: center;
                cursor: pointer;
                box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
            }
            #${IDS.close}:hover {
                background: #555;
            }
            .usc-link {
                color: #1a73e8;
                font-weight: bold;
                text-decoration: underline;
            }
            .usc-link-blue {
                color: blue;
                font-weight: bold;
                text-decoration: underline;
            }
            .usc-comment-highlight {
                color: green;
                font-weight: bold;
                background: #eaffea;
            }
            .usc-coord-highlight {
                background: yellow;
                color: black;
                font-weight: bold;
            }
            .usc-attr-highlight {
                color: red;
                font-weight: bold;
            }
            .usc-hidden-highlight {
                color: purple;
                font-weight: bold;
                background: #f3e8ff;
            }
        `;
        document.head.appendChild(style);
    }

    function escapeHTML(value) {
        return String(value).replace(/[&<>"']/g, character => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;'
        }[character]));
    }

    function decodeEntities(value) {
        const textarea = document.createElement('textarea');
        textarea.innerHTML = value || '';
        return textarea.value;
    }

    function createInputButton(value, className) {
        const button = document.createElement('input');
        button.type = 'button';
        button.value = value;
        button.className = className || 'usc-btn';
        return button;
    }

    function replaceOutsideInjectedMarkup(source, regex, replacer) {
        return source
            .split(/(<a\b[\s\S]*?<\/a>|<span\b[\s\S]*?<\/span>)/gi)
            .map(part => part.startsWith('<a') || part.startsWith('<span') ? part : part.replace(regex, replacer))
            .join('');
    }

    function linkifyAttribute(raw, attribute, className) {
        const pattern = new RegExp(`(${attribute}=&quot;)((?:(?!&quot;)[\\s\\S])*?)(&quot;)`, 'gi');

        return raw.replace(pattern, (match, prefix, encodedUrl, suffix) => {
            const decodedUrl = decodeEntities(encodedUrl);
            const safeUrl = escapeHTML(decodedUrl);
            const safeLabel = encodedUrl;

            return `${prefix}<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="${className}">${safeLabel}</a>${suffix}`;
        });
    }

    function highlightAttributes(raw) {
        return raw
            .replace(/(alt=&quot;(?:(?!&quot;)[\s\S])*?&quot;)/gi, '<span class="usc-attr-highlight">$1</span>')
            .replace(/(title=&quot;(?:(?!&quot;)[\s\S])*?&quot;)/gi, '<span class="usc-attr-highlight">$1</span>')
            .replace(/(class=&quot;(?:(?!&quot;)[\s\S])*?txt_hidden(?:(?!&quot;)[\s\S])*?&quot;)/gi, '<span class="usc-hidden-highlight">$1</span>')
            .replace(/(style=&quot;(?:(?!&quot;)[\s\S])*?(?:color:\s*(?:#ffffff|white)|display:\s*none|visibility:\s*hidden)(?:(?!&quot;)[\s\S])*?&quot;)/gi, '<span class="usc-hidden-highlight">$1</span>');
    }

    function highlightRaw(html) {
        let raw = escapeHTML(decodeEntities(html || ''));

        raw = raw.replace(
            /(&lt;!--[\s\S]*?--&gt;)/g,
            '<span class="usc-comment-highlight">$1</span>'
        );

        raw = highlightAttributes(raw);
        raw = linkifyAttribute(raw, 'href', 'usc-link');
        raw = linkifyAttribute(raw, 'src', 'usc-link-blue');

        raw = replaceOutsideInjectedMarkup(raw, /\bGC[0-9A-Z]{4,8}\b/g, (match, offset, fullText) => {
            if (fullText.slice(Math.max(0, offset - 3), offset).toUpperCase() === 'WP=') {
                return match;
            }

            return `<a href="https://www.geocaching.com/geocache/${match}" target="_blank" rel="noopener noreferrer" class="usc-link">${match}</a>`;
        });

        raw = replaceOutsideInjectedMarkup(
            raw,
            /\b(N|S)\s?\d{1,2}[°\s]+\d{1,2}\.\d{3}\b/gi,
            '<span class="usc-coord-highlight">$&</span>'
        );

        raw = replaceOutsideInjectedMarkup(
            raw,
            /\b(E|W)\s?\d{1,3}[°\s]+\d{1,2}\.\d{3}\b/gi,
            '<span class="usc-coord-highlight">$&</span>'
        );

        raw = replaceOutsideInjectedMarkup(
            raw,
            /\b\d{2,3}\s+\d{2}\.\d{3}\b/g,
            '<span class="usc-coord-highlight">$&</span>'
        );

        return raw;
    }

    function extractComments(html) {
        const comments = (html || '').match(/<!--[\s\S]*?-->/g);
        if (!comments || comments.length === 0) return '';

        return [
            '<h3>HTML Comments Found</h3>',
            `<pre class="usc-comments">${escapeHTML(comments.join('\n\n'))}</pre>`,
            '<hr>'
        ].join('');
    }

    function renderSourceView() {
        const content = document.getElementById(IDS.content);
        if (!content) return;

        content.innerHTML = [
            extractComments(state.combinedHTML),
            `<pre class="usc-pre">${highlightRaw(state.combinedHTML)}</pre>`
        ].join('');
    }

    function renderRenderedView() {
        const content = document.getElementById(IDS.content);
        if (!content) return;
        content.innerHTML = state.combinedHTML || '<h3>No UserSuppliedContent found</h3>';
    }

    async function copyHTML(copyButton) {
        try {
            await navigator.clipboard.writeText(state.combinedHTML || '');
            copyButton.value = 'Copied!';
        } catch (error) {
            copyButton.value = 'Copy failed';
        }

        window.setTimeout(() => {
            copyButton.value = 'Copy HTML';
        }, 900);
    }

    function closePopup() {
        const mainButton = document.getElementById(IDS.button);
        const floatClose = document.getElementById(IDS.close);

        if (state.popup) state.popup.hidden = true;
        if (floatClose) floatClose.hidden = true;
        if (mainButton) mainButton.value = 'Show Source';
    }

    function createPopup() {
        if (state.popup) return state.popup;

        addStyles();

        const popup = document.createElement('div');
        popup.id = IDS.popup;
        popup.hidden = true;

        const header = document.createElement('div');
        header.className = 'usc-header';

        const title = document.createElement('span');
        title.className = 'usc-title';
        title.textContent = 'UserSuppliedContent';

        const copyButton = createInputButton('Copy HTML');
        copyButton.addEventListener('click', () => copyHTML(copyButton));

        const toggleButton = createInputButton('Show Rendered');
        toggleButton.style.color = 'blue';
        toggleButton.addEventListener('click', () => {
            if (state.mode === 'source') {
                state.mode = 'rendered';
                renderRenderedView();
                toggleButton.value = 'Show Raw Source';
            } else {
                state.mode = 'source';
                renderSourceView();
                toggleButton.value = 'Show Rendered';
            }
        });

        const closeButton = createInputButton('X', 'usc-close-btn');
        closeButton.addEventListener('click', closePopup);

        const content = document.createElement('div');
        content.id = IDS.content;

        header.append(title, copyButton, toggleButton, closeButton);
        popup.append(header, content);
        document.body.appendChild(popup);

        const floatClose = document.createElement('div');
        floatClose.id = IDS.close;
        floatClose.textContent = '×';
        floatClose.hidden = true;
        floatClose.addEventListener('click', closePopup);
        document.body.appendChild(floatClose);

        state.popup = popup;
        return popup;
    }

    function buildCombinedHTML() {
        const blocks = Array.from(document.querySelectorAll(SELECTOR_USC));

        state.hasUSC = blocks.length > 0;

        if (blocks.length >= 2) {
            state.combinedHTML = `<h3>Block 2</h3>\n${blocks[1].innerHTML}\n<hr>`;
        } else if (blocks.length === 1) {
            state.combinedHTML = `<h3>Only Block 1 Found</h3>\n${blocks[0].innerHTML}\n<hr>`;
        } else {
            state.combinedHTML = '<h3>No UserSuppliedContent found</h3>';
        }
    }

    function attachSourceButton() {
        if (!state.hasUSC || document.getElementById(IDS.button)) return;

        const showButton = createInputButton('Show Source');
        showButton.id = IDS.button;

        showButton.addEventListener('click', () => {
            buildCombinedHTML();

            const popup = createPopup();
            const floatClose = document.getElementById(IDS.close);
            const opening = popup.hidden;

            popup.hidden = !opening;
            if (floatClose) floatClose.hidden = !opening;
            showButton.value = opening ? 'Hide Source' : 'Show Source';

            if (opening) {
                state.mode = 'source';
                renderSourceView();
            }
        });

        const downloadSection = document.getElementById('Download');
        const firstDD = downloadSection && downloadSection.getElementsByTagName('dd')[0];

        if (firstDD) {
            firstDD.insertBefore(showButton, firstDD.firstChild);
        }
    }

    function refresh() {
        buildCombinedHTML();
        attachSourceButton();
    }

    function start() {
        addStyles();
        refresh();

        let refreshTimer = null;
        const observer = new MutationObserver(() => {
            window.clearTimeout(refreshTimer);
            refreshTimer = window.setTimeout(refresh, 150);
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', start, { once: true });
    } else {
        start();
    }
})();